﻿using System;
using System.Reflection;
using System.Runtime.Serialization;
using Pfz.Extensions;
using Pfz.Remoting.Serializers;
using Pfz.Serialization.BinaryBuiltIn;

namespace Pfz.Serialization
{
	/// <summary>
	/// An already configured binary serializer.
	/// It is capable of fast serialization for registered types and conventional (reflection based)
	/// serialization for non-registered but [Serializable] classes.
	/// </summary>
	public class BinarySerializer:
		ConfigurableSerializerBase
	{
		/// <summary>
		/// Creates and configures a new BinarySerializer.
		/// </summary>
		public BinarySerializer()
		{
			Register(DBNullSerializer.Instance);

			Register(AssemblySerializer.Instance);
			Register(BooleanSerializer.Instance);
			Register(ByteSerializer.Instance);
			Register(DateTimeSerializer.Instance);
			Register(new DoubleSerializer());
			Register(Int32Serializer.Instance);
			Register(Int64Serializer.Instance);
			Register(StringSerializer.Instance);
			Register(TypeSerializer.Instance);
			Register(EventInfoSerializer.Instance);
			Register(MethodInfoSerializer.Instance);
			Register(PropertyInfoSerializer.Instance);

			Register(ArraySerializer<Assembly>.Instance);
			Register(BooleanArraySerializer.Instance);
			Register(ByteArraySerializer.Instance);
			Register(ArraySerializer<DateTime>.Instance);
			Register(ArraySerializer<Double>.Instance);
			Register(ArraySerializer<int>.Instance);
			Register(ArraySerializer<long>.Instance);
			Register(ArraySerializer<string>.Instance);
			Register(ArraySerializer<Type>.Instance);
		}

		/// <summary>
		/// Adds a new type that should be supported using a fast (and compressed) approach, but still using
		/// reflection based serialization.
		/// </summary>
		public void AddDefaultType(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");

			if (CanSerialize(type))
				return;

			if (type.IsArray)
			{
				var serializer = ArraySerializer.GetForElementType(type.GetElementType());
				Register(serializer);

				AddDefaultType(type.GetElementType());
			}
			else
			{
				var serializer = GetItemSerializerForUnregisteredType(type, null);
				if (serializer == null)
					throw new ArgumentException("Can't discover which serializer to use for " + type.FullName);

				Register(serializer);

				if (!typeof(ISerializable).IsAssignableFrom(type))
					foreach(var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
						AddDefaultType(field.FieldType);
			}
		}

		/// <summary>
		/// Serializes null as byte 0.
		/// </summary>
		protected sealed override void OnSerializeNull()
		{
			Stream.WriteByte(0);
		}

		/// <summary>
		/// Serializes a reference by writing the byte 1 and then the id as a compressed int.
		/// </summary>
		/// <param name="id"></param>
		protected sealed override void OnSerializeReference(int id)
		{
			Stream.WriteByte(1);
			Stream.WriteCompressedInt32(id);
		}

		/// <summary>
		/// Gets a serializer for a non-registered item. [Serializable] items or remoting items.
		/// </summary>
		protected override IItemSerializer GetItemSerializerForUnregisteredType(Type type, Type deserializerWillExpectType)
		{
			if (!type.IsSerializable && !type.IsArray)
				return null;

			if (Stream != null)
			{
				if (type != deserializerWillExpectType || !type.IsValueType)
				{
					Stream.WriteByte(2);
					TypeSerializer.Instance.Serialize(this, type);
				}
			}

			if (type.IsArray)
			{
				var serializer = ArraySerializer.GetForElementType(type.GetElementType());
				return serializer;
			}

			var result = _GetItemSerializer(type);
			return result;
		}

		/// <summary>
		/// Serializes a type by its id.
		/// </summary>
		protected sealed override void OnSerializeType(int typeIndex, Type type, Type deserializerWillExpectType)
		{
			if (type.IsValueType && type == deserializerWillExpectType)
				return;

			Stream.WriteCompressedInt32(typeIndex+4);
		}

		/// <summary>
		/// Deserializes an object.
		/// </summary>
		protected override object OnDeserialize(Type expectedType)
		{
			Type effectiveType = null;
			if (expectedType != null && expectedType.IsValueType)
			{
				if (!(expectedType.IsGenericType && expectedType.GetGenericTypeDefinition() == typeof(Nullable<>)))
					effectiveType = expectedType;
			}

			IItemSerializer itemDeserializer = null;
			if (effectiveType == null)
			{
				var stream = Stream;
				int value = stream.ReadCompressedInt32();

				switch(value)
				{
					case 0:
						return null;

					case 1:
					{
						int referenceId = stream.ReadCompressedInt32();
						return ResolveReference(referenceId);
					}

					case 2:
					{
						effectiveType = TypeSerializer.Instance.Deserialize(this);
						itemDeserializer = GetItemDeserializerForUnregisteredType(effectiveType, expectedType);
						break;
					}

					case 3:
						effectiveType = typeof(object);
						itemDeserializer = RemotingObjectSerializer.Instance;
						break;

					default:
						effectiveType = GetTypeById(value-4);
						break;
				}
			}

			using(var serializationIdGenerator = CreateIdGenerator(effectiveType))
			{
				if (itemDeserializer == null)
				{
					var itemDeserializerReference = TryGetItemSerializer(effectiveType);
					itemDeserializer = itemDeserializerReference.ItemSerializer;

					if (itemDeserializer == null)
						itemDeserializer = GetItemDeserializerForUnregisteredType(effectiveType, expectedType);
				}

				object result = itemDeserializer.Deserialize(this);
				serializationIdGenerator.Result = result;
				return result;
			}
		}

		/// <summary>
		/// Gets the deserializer for the given type, considering it is not a registered one.
		/// </summary>
		protected virtual IItemSerializer GetItemDeserializerForUnregisteredType(Type type, Type expectedType)
		{
			if (type.IsArray)
			{
				var deserializer = ArraySerializer.GetForElementType(type.GetElementType());
				return deserializer;
			}

			if (!type.IsSerializable)
				throw new SerializationException("There is not a registered type for " + type.FullName + " and it is not [Serializable].");

			return _GetItemSerializer(type);
		}

		private static IItemSerializer _GetItemSerializer(Type type)
		{
			if (typeof(ISerializable).IsAssignableFrom(type))
				type = typeof(SerializableInterfaceSerializer<>).MakeGenericType(type);
			else
				type = typeof(SerializableSerializer<>).MakeGenericType(type);

			var field = type.GetField("Instance");
			var untypedResult = field.GetValue(null);
			var result = (IItemSerializer)untypedResult;
			return result;
		}
	}
}
